package com.tools.db.jdbc;

import com.tools.common.object.DateKit;
import com.tools.common.object.Note;
import com.tools.common.object.string.StrKit;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * SQL 监视器拦截器插件
 * */
@Note("SQL 监视器拦截器插件")
@Intercepts({
        @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),
        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class })
})
@Component
public class SQLMonitorInterceptor implements Interceptor {

    @Note("是否开启打印结果条数")
    private final boolean printResult;

    private static final String BORDER = StrKit.addString("-", 100);
    private static final Logger LOGGER = LoggerFactory.getLogger(SQLMonitorInterceptor.class);

    /* *******************************************************************************************
     *
     *          构造器与 Getter
     *
     * *******************************************************************************************
     * */

    public SQLMonitorInterceptor() {
        this.printResult = false;
    }

    public SQLMonitorInterceptor(boolean printResult) {
        this.printResult = printResult;
    }

    public boolean isPrintResult() {
        return printResult;
    }

    /* *******************************************************************************************
     *
     *          核心逻辑
     *
     * *******************************************************************************************
     * */

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取执行参数
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        String sqlId = mappedStatement.getId();
        Object parameter = invocation.getArgs()[1];
        // 获取 BoundSql，构建完整参数的 SQL
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        String srcSql = boundSql.getSql();
        String completeSql = private_buildCompleteSql(boundSql, srcSql, mappedStatement);
        //发送请求到数据库对 SQL 进行执行，并统计耗时
        long startTime = System.currentTimeMillis();
        Object result = invocation.proceed();
        long endTime = System.currentTimeMillis();
        long sqlExecTime = endTime - startTime;
        //输出统计结果行数，并根据 printResult 的值决定是否打印结果
        this.private_handleResultPrint(sqlId, completeSql, sqlExecTime, result);
        return result;
    }



    /* *******************************************************************************************
     *
     *          私有逻辑
     *
     * *******************************************************************************************
     * */

    @Note("构建完整的 SQL 语句，将 ? 占位符替换为真实的参数值")
    private static String private_buildCompleteSql(BoundSql boundSql, String srcSql, MappedStatement mappedStatement) {
        srcSql = srcSql.replaceAll("\\s+", " ");
        // 获取mapper里面方法上的参数
        Object sqlParameter = boundSql.getParameterObject();
        // sql语句里面需要的参数
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if(parameterMappings == null || parameterMappings.isEmpty() || sqlParameter == null) {
            return srcSql;
        }
        //单个参数或已有类型处理器的
        Configuration configuration = mappedStatement.getConfiguration();
        if(configuration.getTypeHandlerRegistry().hasTypeHandler(sqlParameter.getClass())) {
            return srcSql.replaceFirst("\\?", private_oneParamToStringValue(sqlParameter));
        }
        //其他容器、pojo 类型
        MetaObject metaObject = configuration.newMetaObject(sqlParameter);
        for (ParameterMapping parameterMapping : parameterMappings) {
            // 按顺序把?替换成对应的值
            String propertyName = parameterMapping.getProperty();
            if (metaObject.hasGetter(propertyName)) {
                Object obj = metaObject.getValue(propertyName);
                srcSql = srcSql.replaceFirst("\\?", private_oneParamToStringValue(obj));
                continue;
            }
            if (boundSql.hasAdditionalParameter(propertyName)) {
                Object obj = boundSql.getAdditionalParameter(propertyName);
                srcSql = srcSql.replaceFirst("\\?", private_oneParamToStringValue(obj));
            }
        }
        return srcSql;
    }


    @Note("将获取的单个参数转为复合 SQL 打印的字符串形式")
    private static String private_oneParamToStringValue(Object obj) {
        if(obj == null) return "null";
        if (obj instanceof String || obj instanceof Character) {
            return  '\'' + obj.toString() + '\'';
        }
        if (obj instanceof Number || obj instanceof Boolean || obj instanceof Enum) {
            return obj.toString();
        }
        if(obj instanceof Date) {
            return DateKit.format((Date) obj);
        }
        if(obj instanceof LocalDateTime) {
            return DateKit.format((LocalDateTime) obj);
        }
        if(obj instanceof LocalDate) {
            return DateKit.format((LocalDate) obj);
        }
        if(obj instanceof LocalTime) {
            return DateKit.format((LocalTime) obj);
        }
        return obj.toString();
    }

    @Note("打印日志。输出方法 ID、填充参数后的 SQL、执行时间、结果条数，并根据配置参数是否需要输出结果")
    private void private_handleResultPrint(String id, String sql, long execTime, Object result) {
        if(result == null) {
            if(this.printResult) {
                LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: 0\n【Result】: null\n{}{}{}\n",
                        BORDER, id, BORDER, sql, execTime, BORDER, BORDER, BORDER);
            } else {
                LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: 0\n{}{}{}\n",
                        BORDER, id, BORDER, sql, execTime, BORDER, BORDER, BORDER);
            }
            return;
        }
        if(result instanceof Collection) {
            Collection c = (Collection) result;
            if(this.printResult) {
                StringBuilder resultString = new StringBuilder();
                for(Object o : c) {
                    resultString.append('\n').append(o.toString());
                }
                LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: {}\n【Result】: {}\n{}{}{}\n",
                        BORDER, id, BORDER, sql, execTime, c.size(), resultString, BORDER, BORDER, BORDER);
            } else {
                LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: {}\n{}{}{}\n",
                        BORDER, id, BORDER, sql, execTime, c.size(), BORDER, BORDER, BORDER);
            }
            return;
        }
        if(result instanceof Map) {
            Map m = (Map) result;
            if(this.printResult) {
                StringBuilder resultString = new StringBuilder();
                for(Object o : m.entrySet()) {
                    Map.Entry e = (Map.Entry) o;
                    resultString.append('\n').append(e.getKey()).append(" >>> ").append(e.getValue());
                }
                LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: {}\n【Result】: {}\n{}{}{}\n",
                        BORDER, id, BORDER, sql, execTime, m.size(), resultString, BORDER, BORDER, BORDER);
            } else {
                LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: {}\n{}{}{}\n",
                        BORDER, id, BORDER, sql, execTime, m.size(), BORDER, BORDER, BORDER);
            }
            return;
        }
        if(this.printResult) {
            LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: 1\n【Result】: {}\n{}{}{}\n",
                    BORDER, id, BORDER, sql, execTime, result, BORDER, BORDER, BORDER);
            return;
        }
        LOGGER.info("\n{} [{}] {}\n【SQL】 ==> : {}\n【ExecTime】: {} ms\n【Count】: 1\n{}{}{}\n",
                BORDER, id, BORDER, sql, execTime, BORDER, BORDER, BORDER);
    }
}

